"""
:synopsis: Assign NGUID
:authors: Riley Baird (OK), Emma Baker (OK)
"""
import logging

# lib import
from ...lib.gdbsession import DummyEditor
from ...lib.session import config
import os
import random
import datetime
import arcpy
from arcgis.features import GeoAccessor, GeoSeriesAccessor
from typing import Literal


_logger = logging.getLogger(__name__)

random_start_phrase = random.choice(['Make it so!', 'Hello World!', 'As the prophecy foretold...', 'Greetings earthlings.', 'All are bases are belong to us.', 'The Jig is up!'])

tool_switch: str = "ASSIGN_NGUID"
error_list = ["PLEASE SELECT A FEATURE CLASS", "NO FIELDS FOUND IN SELECTED FC", "--PLEASE SELECT FIELD--"]

## Custom Tool Parameter Information
class CustomToolParameterInfo:
    def __init__(self):
        ## Parameter Indexes
        self.fc_std_name_idx = 0
        self.fc_path_idx = self.fc_std_name_idx + 1
        self.assign_method_idx = self.fc_path_idx + 1
        self.old_to_new_format_idx = self.assign_method_idx + 1
        self.delete_old_field_idx = self.old_to_new_format_idx + 1
        self.keep_field_name_idx = self.delete_old_field_idx + 1
        self.field_table_idx = self.keep_field_name_idx + 1
        # Field Table Parameter Indexes
        self.sep_string_idx = 0
        self.user_nguid_field_idx = self.sep_string_idx + 1
        self.user_local_field_idx = self.user_nguid_field_idx + 1
        self.user_agency_field_idx = self.user_local_field_idx + 1
        self.required_dataset_name: str = config.gdb_info.required_dataset_name # "NG911"
        self.optional_dataset_name: str = config.gdb_info.optional_dataset_name # "OptionalLayers"
        ## Std Field Names
        self.local_field = config.fields.local_id
        self.agency_field = config.fields.agency_id
        self.fc_std_name_list = list(config.required_feature_class_names)
        self.field_option_list = [error_list[0]]
        ## Tool Parameters
        self.tool_method_option_list = ["NGUID", "LOCAL", "SEQUENTIAL"]
        self.gis_urn_default_value = config.gdb_info.nguid_urn_prefix
custom_param = CustomToolParameterInfo


class AssignNGUID(object):
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "0 - Assign NGUID Field"
        self.description = "Assigns new NGUIDs and/or converts old NGUIDs"
        self.canRunInBackground = False
        self.category = "1 - Prep"

    def getParameterInfo(self):
        """Define parameter definitions"""

        params = []

        fc_std_name = arcpy.Parameter(
            displayName=f"Standard Feature Class Name",
            name="fc_std_name",
            datatype="GPString",
            parameterType="Required",
            direction="Input")
        fc_std_name.filter.list = custom_param().fc_std_name_list
        fc_std_name.value = custom_param().fc_std_name_list[0]
        params += [fc_std_name]

        fc_path = arcpy.Parameter(
            displayName=f"\nUser-specified Feature Class",
            name="fc_path",
            datatype="DEFeatureClass",
            parameterType="Required",
            direction="Input")
        params += [fc_path]

        assign_method = arcpy.Parameter(
            displayName=f"\nAssign Method",
            name="assign_method",
            datatype="GPString",
            parameterType="Required",
            direction="Input")
        assign_method.filter.list = custom_param().tool_method_option_list
        assign_method.value = custom_param().tool_method_option_list[-1]
        params += [assign_method]

        old_to_new_format = arcpy.Parameter(
            displayName=f"Old format (`@`) to New format (`:`)?",
            name="old_to_new_format",
            datatype="GPBoolean",
            parameterType="Optional",
            direction="Input")
        # old_to_new_format.message = f"A user-specified `NGUID` is necessary switch formats."
        old_to_new_format.value = False
        params += [old_to_new_format]

        delete_old_field = arcpy.Parameter(
            displayName=f"Delete old `NGUID` from user-specified FC?",
            name="delete_old_field",
            datatype="GPBoolean",
            parameterType="Optional",
            direction="Input")
        # delete_old_field.message = f"A user-specified `NGUID` is necessary for the following processes.\n\t1. {method_option_list[0]} assign method\n\t2. Switching from old format (`@`) to new format (`:`)"
        delete_old_field.value = True
        params += [delete_old_field]

        keep_field_name = arcpy.Parameter(
            displayName=f"Field Name for OLD `NGUID` field name when `NGUID` Standard Name already exists\n\tFor assign methods: `{custom_param().tool_method_option_list[1]}` and `{custom_param().tool_method_option_list[-1]}`",
            name="keep_field_name",
            datatype="GPString",
            parameterType="Optional",
            direction="Input")
        keep_field_name.value = f"NGUID_OLD"
        params += [keep_field_name]

        field_table_parameter = arcpy.Parameter(
            displayName=f"\n-----   === Fields for assignment ===   -----",
            name="field_table_parameter",
            datatype="GPValueTable",
            parameterType="Optional",
            direction="Input")
        field_table_parameter.columns = [['GPString', f"\t", "ReadOnly"], ['GPString', f'User-specified `NGUID` Field'], ['GPString', f'User-specified `{custom_param().local_field.name}` Field'], ['GPString', f'User-specified `{custom_param().agency_field.name}` Field']]
        parameter_value_list = [f"\t"] + [custom_param().field_option_list[0]]*(len(field_table_parameter.columns)-1)
        field_table_parameter.filters[custom_param().user_nguid_field_idx].type = "ValueList"
        field_table_parameter.filters[custom_param().user_nguid_field_idx].list = custom_param().field_option_list
        field_table_parameter.filters[custom_param().user_local_field_idx].type = "ValueList"
        field_table_parameter.filters[custom_param().user_local_field_idx].list = custom_param().field_option_list
        field_table_parameter.filters[custom_param().user_agency_field_idx].type = "ValueList"
        field_table_parameter.filters[custom_param().user_agency_field_idx].list = custom_param().field_option_list
        field_table_parameter.values = [parameter_value_list]
        params += [field_table_parameter]

        updated_fc_path = arcpy.Parameter(
            displayName=f"Updated Feature Class",
            name="updated_feature_class",
            datatype="DEFeatureClass",
            parameterType="Derived",
            direction="Output")
        params += [updated_fc_path]

        return params

    def isLicensed(self):
        """Set whether tool is licensed to execute."""
        return True

    def updateParameters(self, parameters):
        """Modify the values and properties of parameters before internal
        validation is performed.  This method is called whenever a parameter
        has been changed."""
        ## table parameter
        fc_std_name = parameters[custom_param().fc_std_name_idx]
        fc_path = parameters[custom_param().fc_path_idx]
        assign_method = parameters[custom_param().assign_method_idx]
        delete_old_field = parameters[custom_param().delete_old_field_idx]
        keep_field_name = parameters[custom_param().keep_field_name_idx]
        field_table = parameters[custom_param().field_table_idx]

        std_fc = config.get_feature_class_by_name(fc_std_name.value)
        unique_id_name = std_fc.unique_id.name

        if fc_std_name.altered and not fc_std_name.hasBeenValidated:
            std_fc = config.get_feature_class_by_name(fc_std_name.value)
            unique_id_name = std_fc.unique_id.name
            field_table.values = field_table.values
            filter_field_list = field_table.filters[custom_param().user_nguid_field_idx].list
            if not delete_old_field.value and assign_method.value in custom_param().tool_method_option_list[1:] and unique_id_name in filter_field_list:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = True
            else:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = False
        elif fc_path.value and fc_path.altered and not fc_path.hasBeenValidated:
            value_list = field_table.values[0]
            try:
                field_list = [error_list[-1]]
                fc_field_list = [field.name for field in arcpy.Describe(fc_path.valueAsText).fields if 'object' not in field.name.lower() and 'shape' not in field.name.lower()]
                if len(fc_field_list) > 0:
                    field_list = field_list + fc_field_list
            except:
                field_list = [error_list[1]]
            field_table.filters[custom_param().user_nguid_field_idx].list = field_list
            field_table.filters[custom_param().user_local_field_idx].list = field_list
            field_table.filters[custom_param().user_agency_field_idx].list = field_list
            value_list[custom_param().user_nguid_field_idx] = field_list[0]
            value_list[custom_param().user_local_field_idx] = field_list[0]
            value_list[custom_param().user_agency_field_idx] = field_list[0]
            field_table.values = [value_list]
            if not delete_old_field.value and assign_method.value in custom_param().tool_method_option_list[1:] and unique_id_name in field_list:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = True
            else:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = False
        elif assign_method.altered and not assign_method.hasBeenValidated:
            field_table.values = field_table.values
            filter_field_list = field_table.filters[custom_param().user_nguid_field_idx].list
            if not delete_old_field.value and assign_method.value in custom_param().tool_method_option_list[1:] and unique_id_name in filter_field_list:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = True
            else:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = False
        elif not delete_old_field.value and delete_old_field.altered and not delete_old_field.hasBeenValidated:
            field_table.values = field_table.values
            field_list = field_table.filters[custom_param().user_nguid_field_idx].list
            if assign_method.value in custom_param().tool_method_option_list[1:] and unique_id_name in field_list:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = True
            else:
                keep_field_name.value = keep_field_name.value
                keep_field_name.enabled = False
        else:
            field_table.values = field_table.values
            keep_field_name.value = keep_field_name.value
            keep_field_name.enabled = False

        return

    def updateMessages(self, parameters):
        """Modify the messages created by internal validation for each tool
        parameter.  This method is called after internal validation."""
        ## table parameter

        fc_name_param = parameters[custom_param().fc_std_name_idx]
        fc = config.get_feature_class_by_name(fc_name_param.value)
        fc_fields = fc.fields.values()
        fc_name = fc.name
        unique_id_name = fc.unique_id
        fc_path = parameters[custom_param().fc_path_idx]
        assign_method = parameters[custom_param().assign_method_idx]
        format_switch = parameters[custom_param().old_to_new_format_idx]
        delete_old_field = parameters[custom_param().delete_old_field_idx]
        keep_field_name = parameters[custom_param().keep_field_name_idx]
        field_table = parameters[custom_param().field_table_idx]
        user_nguid_field = field_table.values[0][custom_param().user_nguid_field_idx]
        user_local_field = field_table.values[0][custom_param().user_local_field_idx]
        user_agency_field = field_table.values[0][custom_param().user_agency_field_idx]
        if not fc_path.value and fc_path.hasBeenValidated:
            fc_path.setErrorMessage(f"{error_list[0]}")
        if format_switch.value and user_nguid_field in error_list:
            format_switch.setErrorMessage(f"To switch from the old format (`@`) to the new format (`:`), please specify `NGUID` field")
        if not delete_old_field.value:
            keep_field_name_value = keep_field_name.value
            if fc_path.value and fc_path.hasBeenValidated:
                fc_field_list = [field.name.lower() for field in arcpy.Describe(fc_path.valueAsText).fields]
                if keep_field_name_value.lower() in fc_field_list:
                    delete_old_field.setErrorMessage(f"Selected Field Name for old `NGUID` already exists in feature class. Please select a different field name string.")
        if assign_method.value == custom_param().tool_method_option_list[0] and user_nguid_field in error_list:
            assign_method.setErrorMessage(f"With assign method set to `{assign_method.value}`, please specify `NGUID` field")
        if assign_method.value == custom_param().tool_method_option_list[1] and user_local_field in error_list:
            assign_method.setErrorMessage(f"With assign method set to `{assign_method.value}`, please specify `{custom_param().local_field.name}` field")
        if assign_method.value != custom_param().tool_method_option_list[0] and user_agency_field in error_list:
            field_table.setWarningMessage(f"No `{custom_param().agency_field.name}` field specified. `NGUID` will be assigned using a blank sting for `Agency` part.")

        return


    def execute(self, parameters, messages):
        """The source code of the tool."""
        ## Process parameters from user-specified values
        fc_std_name: str = parameters[custom_param().fc_std_name_idx].value
        fc_path: str = parameters[custom_param().fc_path_idx].valueAsText
        assign_method: Literal["NGUID", "LOCAL", "SEQUENTIAL"] = parameters[custom_param().assign_method_idx].value
        format_switch: bool = parameters[custom_param().old_to_new_format_idx].value
        delete_old_field: bool = parameters[custom_param().delete_old_field_idx].value
        keep_field_name: str = parameters[custom_param().keep_field_name_idx].value
        field_table = parameters[custom_param().field_table_idx]
        table_value_list = field_table.values[0]
        user_nguid_field: str | None = table_value_list[custom_param().user_nguid_field_idx] if table_value_list[custom_param().user_nguid_field_idx] not in error_list else None # String
        user_local_field: str | None = table_value_list[custom_param().user_local_field_idx] if table_value_list[custom_param().user_local_field_idx] not in error_list else None # String
        user_agency_field: str | None = table_value_list[custom_param().user_agency_field_idx] if table_value_list[custom_param().user_agency_field_idx] not in error_list else None # String

        ## Checking gdb for NG911 dataset
        arcpy.AddMessage(f"{random_start_phrase}")

        # `NGUID` field analysis
        # Add `NGUID` field if it already doesn't exist in fc
        fc_field_list: list[str] = [field.name for field in arcpy.Describe(fc_path).fields]
        fc = config.get_feature_class_by_name(fc_std_name)
        # fc_fields = fc.fields.values()
        # fc_name = fc.name
        unique_id_name = fc.unique_id.name
        std_nguid_field_ns = config.get_field_by_name(unique_id_name)
        std_nguid_field_type = std_nguid_field_ns.type
        std_nguid_field_length = std_nguid_field_ns.length
        nguid_field_exists: bool = any(name.upper() == unique_id_name.upper() for name in fc_field_list)

        arcpy.AddMessage(f"\nBeginning Standard `NGUID` Field analysis for `{unique_id_name}` in user-specified Feature Class `{os.path.basename(fc_path)}`...")

        existing_std_nguid_switch = False
        # new_field_name = f"{unique_id_name}_OLD"
        end_string = datetime.datetime.now().strftime('%Y%m%d_%H%M')
        if len(end_string) > 15:
            end_string = end_string[:15]
        new_field_name = f"{unique_id_name}_{end_string}"
        if nguid_field_exists:
            if user_nguid_field.upper() == unique_id_name.upper():
                # User-specified NGUID field is the already-existing correctly-named (case-insensitive) NGUID field
                alter_field = user_nguid_field
                user_nguid_field = new_field_name
            else:
                existing_std_nguid_switch = True
                if not delete_old_field and assign_method in custom_param().tool_method_option_list[1:]:
                    new_field_name = keep_field_name
                alter_field = unique_id_name
            arcpy.AddWarning(
                "There is an NGUID field that matches the standard name already, so to ensure the NGUID field meets all Oklahoma Standards:"
                f"\n\t- The EXISTING field will have its name CHANGED to `{new_field_name}`"
                "\n\t- A NEW standard field will be CREATED"
            )
            arcpy.AlterField_management(in_table=fc_path, field=alter_field, new_field_name=new_field_name)

        arcpy.AddField_management(in_table=fc_path,
                                  field_name=unique_id_name,
                                  field_type=std_nguid_field_type,
                                  field_length=std_nguid_field_length,
                                  field_alias=unique_id_name)

        # Create update cursor for fc
        update_cursor_field_list = ["OBJECTID", unique_id_name]
        if existing_std_nguid_switch:
            update_cursor_field_list = update_cursor_field_list + [new_field_name]
        if user_nguid_field:
            update_cursor_field_list = update_cursor_field_list + [user_nguid_field]
        if user_local_field:
            update_cursor_field_list = update_cursor_field_list + [user_local_field]
        if user_agency_field:
            update_cursor_field_list = update_cursor_field_list + [user_agency_field]

        # Use update cursor to create NGUID string based on user-specified `assign_nguid_switch` and available data
        try:
            workspace: str = arcpy.Describe(fc_path).workspace.catalogPath
            editor = arcpy.da.Editor(workspace)
        except Exception as exc:
            _logger.warning(f"Failed to determine workspace for '{fc_path}'.", exc_info=exc)
            editor = DummyEditor()
        with editor:
            update_cursor = arcpy.da.UpdateCursor(fc_path, update_cursor_field_list)
            for key, row in enumerate(update_cursor):
                object_id = row[0]
                sequential_cnt = key + 1
                agencyid_value = ''
                if user_agency_field in update_cursor_field_list:
                    agencyid_value = row[update_cursor_field_list.index(user_agency_field)]
                    if not agencyid_value or agencyid_value not in list(config.domains.AGENCYID.entries.keys()):
                        agencyid_value = ''
                local911_value = sequential_cnt
                if assign_method == "NGUID":  # "NGUID"
                    if user_nguid_field in update_cursor_field_list:
                        user_nguid_value = row[update_cursor_field_list.index(user_nguid_field)]
                    else:
                        user_nguid_value = None
                    if user_nguid_value:
                        if format_switch:
                            local911_value = user_nguid_value.split("@")[0].split("_")[-1]
                            agencyid_value = user_nguid_value.split("@")[-1]
                        else:
                            local911_value = user_nguid_value.split(":")[-2]
                            agencyid_value = user_nguid_value.split(":")[-1]
                    else:
                        arcpy.AddWarning(f"No `NGUID` value retrieved from `{os.path.basename(fc_path)}` for OBJECT_ID `{object_id}`...")
                        arcpy.AddWarning(f"...using Sequential count for `LOCAL911UniqueID` and using Blank String for `Agency_ID` parts of the `NGUID` string...")
                elif assign_method == "LOCAL":  # "LOCAL"
                    if user_local_field in update_cursor_field_list:
                        local911_value = row[update_cursor_field_list.index(user_local_field)]
                        if not local911_value:
                            local911_value = sequential_cnt
                            arcpy.AddWarning(f"No `Local` value retrieved from `{os.path.basename(fc_path)}` for OBJECT_ID `{object_id}`...")
                            arcpy.AddWarning(f"...using Sequential count for `LOCAL911UniqueID`...")
                else:  # "SEQUENTIAL"
                    local911_value = sequential_cnt
                nguid_value = ":".join([str(custom_param().gis_urn_default_value), fc_std_name, str(local911_value), str(agencyid_value)])
                row[1] = nguid_value
                update_cursor.updateRow(row)

        if delete_old_field:
            if existing_std_nguid_switch:
                arcpy.AddMessage(f"Deleting old `NGUID` field `{new_field_name}`...")
                arcpy.DeleteField_management(in_table=fc_path, drop_field=new_field_name)
            else:
                arcpy.AddMessage(f"Deleting old `NGUID` field `{user_nguid_field}`...")
                arcpy.DeleteField_management(in_table=fc_path, drop_field=user_nguid_field)

        arcpy.AddMessage(f"New Standard `NGUID` field {unique_id_name} added to {os.path.basename(fc_path)}.")
        parameters[-1].value = fc_path

        return


if __name__ == "__main__":
    raise Exception("This module is a dependency of an ArcGIS Python Toolbox and should not be executed directly.")